package com.ihy.config;

import com.ihy.filter.JwtAuthenticationTokenFilter;
import com.ihy.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/*
 *  Spring Security配置类
 *
 * 1.它会拿到用户输入的用户名和密码；
 * 2.根据用户名通过UserDetailsService 的 loadUserByUsername(username)方法获得一个用户对象；
 * 3.获得一个UserDetails 对象，获得内部的成员属性password；
 * 4.通过PasswordEncoder 的 matches(s1, s2) 方法对比用户的输入的密码和第3步的密码；
 * 5.匹配成功；
 *
 * Configuration注解:
 * 指示类声明一个或多个@Bean方法，并可由Spring容器处理以在运行时为这些Bean生成Bean定义和服务请求
 *
 * EnableWebSecurity注解:
 * 1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略。
 * 2: 加载了AuthenticationConfiguration, 配置了认证信息。
 *
 * EnableGlobalMethodSecurity注解：
 * 开启注解的权限控制，默认是关闭的。
 * prePostEnabled：使用表达式实现方法级别的控制，如：@PreAuthorize("hasRole('ADMIN')")
 * securedEnabled: 开启 @Secured 注解过滤权限，如：@Secured("ROLE_ADMIN")
 * jsr250Enabled: 开启 @RolesAllowed 注解过滤权限，如：@RolesAllowed("ROLE_ADMIN")
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //注入需要的实现类
    /*
        Qualifier注解:
        表明了哪个实现类才是我们所需要的，添加@Qualifier注解。
        需要注意的是@Qualifier的参数名称为我们之前定义@Service注解的名称之一。

        Autowired注解:
        将构造函数、字段、setter方法或config方法标记为由Spring的依赖注入工具自动连接。
        Autowired注解是按照类型（byType）装配依赖对象，默认情况下它要求依赖对象必须存在，如果允许null值，可以设置它的required属性为false。
        如果我们想使用按照名称（byName）来装配，可以结合@Qualifier注解一起使用。(通过类型匹配找到多个candidate,在没有@Qualifier、@Primary注解的情况下，会使用对象名作为最后的fallback匹配)

        Resource注解:
        Resource默认按照ByName自动注入，由J2EE提供，需要导入包javax.annotation.Resource。
        @Resource有两个重要的属性：name和type，而Spring将@Resource注解的name属性解析为bean的名字，而type属性则解析为bean的类型。
        所以，如果使用name属性，则使用byName的自动注入策略，而使用type属性时则使用byType自动注入策略。
        如果既不制定name也不制定type属性，这时将通过反射机制使用byName自动注入策略。

        UserDetailsService：
        加载用户特定数据的核心接口。
        它在整个框架中用作用户DAO，是DaoAuthenticationProvider使用的策略。
        该接口只需要一个只读方法，这简化了对新数据访问策略的支持

        LoginSuccessHandler： 登录成功处理器
        LoginFailHandler： 登录失败处理器
        NoRightAccessHandler： 访问被拒绝处理器
        NotLoginEntryPoint： 未登录处理器
        ReqLogoutSuccessHandler： 注销成功处理器
    */
    @Qualifier("userDetailsServiceImpl")
    //变量注入 更简洁 但是不能有效的指明依赖。
    @Autowired
    private UserDetailsService userDetailsService;
    //构造方法进行注入  在使用构造器方式时已经显式注明必须强制注入。通过强制指明依赖注入来保证这个类的运行。
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    public void setLoginSuccessHandler(LoginSuccessHandler loginSuccessHandler) {
        this.loginSuccessHandler = loginSuccessHandler;
    }

    //set方法进行注入 在使用set方式时，这是一种选择注入，可有可无，即使没有注入这个依赖，那么也不会影响整个类的运行。
    private LoginFailHandler loginFailHandler;

    @Autowired
    public void setLoginFailHandler(LoginFailHandler loginFailHandler) {
        this.loginFailHandler = loginFailHandler;
    }

    @Autowired
    private NoRightAccessHandler noRightAccessHandler;
    @Autowired
    private NotLoginEntryPoint notLoginEntryPoint;
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private CustomizeSessionInformationExpiredStrategy customizeSessionInformationExpiredStrategy;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private InvalidSessionHandler invalidSessionHandler;

    /**
     * 覆盖忽略拦截方法
     * <p>
     * Override注解：
     * 指示方法声明旨在重写父类型中的方法声明。如果使用此注解类型对方法进行注解，则需要编译器生成错误消息，除非至少满足以下条件之一：
     * 该方法重写或实现在父类型中声明的方法。
     * 该方法具有一个重写的签名，该签名等同于在对象中声明的任何公共方法的签名。
     *
     * @param web WebSecurity由WebSecurityConfiguration创建，以创建名为Spring Security Filter Chain（springSecurityFilterChain）的FilterChainProxy。
     *            springSecurityFilterChain是DelegatingFilterProxy委托给的过滤器。
     *            可以通过创建WebSecurityConfigurer或更可能通过重写WebSecurityConfigurerAdapter来定制WebSecurity。
     */
    @Override
    public void configure(WebSecurity web) {
        //web.ignoring().antMatchers("/ForgetPassword/**").antMatchers("/Verification/**");
        //对于在header里面增加token等类似情况，放行所有OPTIONS请求。
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
        web.ignoring().antMatchers("/css/**", "/js/**", "/jpg/**","/img/**","/ico/**");
    }

    //跨域请求配置
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOriginPattern("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader("*");
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //登录配置 制定哪些权限可以访问哪些接口
        http.authorizeRequests()
                //表示访问 这个接口，需要具备 admin 这个角色
                //.antMatchers("/Login/signin").hasRole("ADMIN")
                //表示访问 这个接口,直接放行。
                .antMatchers("/ForgetPassword/**").permitAll()
                .antMatchers("/Register/**").permitAll()
                .antMatchers("/Verification/**").permitAll()
                .antMatchers("/Login/**").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/logout").permitAll()
                .antMatchers("/QQ/**").permitAll()
                .antMatchers("/").permitAll()
                .antMatchers("/clause").permitAll()
                .antMatchers("/about").permitAll()
                //.antMatchers("/login").permitAll()
                //以"/aba"开始的URL，并需拥有 "ROLE_ADMIN" 角色权限和 "ROLE_DBA" 角色权限,这里不需要写"ROLE_"前缀；
                //.antMatchers("/dba/**").access("hasRole('ADMIN') and hasRole('USER')")
                //表示只接受post请求访问/api/data 需要权限add
                //.antMatchers(HttpMethod.POST, "/api/data").hasAuthority("add")
                //表示只接受get请求访问/api/data 需要权限query
                //.antMatchers(HttpMethod.GET, "/api/data").hasAuthority("query");
                // 跨域预检请求
                //.antMatchers(HttpMethod.OPTIONS,"/**").permitAll()
                // 表示剩余的其他接口，登录之后才能访问
                .anyRequest().authenticated()
                //限制同一账号只能在手机端电脑端各登陆一次
                .and().sessionManagement()
                //超时处理
                .invalidSessionStrategy(invalidSessionHandler)
                .maximumSessions(2)
                //会话信息过期策略(账号被挤下线)
                .expiredSessionStrategy(customizeSessionInformationExpiredStrategy);
        //禁用csrf
        http.csrf().disable();
        //允许跨域，如果springboot/springmvc已有跨域配置，自动采用springboot/springmvc跨域配置
        http.cors();
        /*
            指定登录请求路径，该路径会走 UsernamePasswordAuthenticationFilter 进行登录操作。必须是POST请求，而且是FORM表单传参，不能JSON传参。
            1.开启表单登录
            2.登录处理接口
            3.登录成功处理器
            4.登录失败处理器
          */
        http.formLogin().loginProcessingUrl("/Login/signIn").successHandler(loginSuccessHandler).failureHandler(loginFailHandler);
        //身份验证处理器
        http.httpBasic().authenticationEntryPoint(notLoginEntryPoint);
        //访问被拒处理器
        http.exceptionHandling().accessDeniedHandler(noRightAccessHandler);
        /*
         1.开启注销
         2.注销成功处理器
         */
        http.logout().logoutUrl("/Login/logout").logoutSuccessHandler(logoutSuccessHandler).permitAll();
         /*
         在UsernamePasswordAuthenticationFilter之前添加 JwtAuthenticationTokenFilter，让所有请求先到JwtAuthenticationTokenFilter。
         */
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 禁用缓存
        http.headers().cacheControl();
        //基于token 禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //将默认加载的登录页配置删除
        http.removeConfigurer(DefaultLoginPageConfigurer.class);
    }

    //定义认证用户信息获取来源、密码校验规则,用户名不存在处理方法
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        /*
            默认情况下，如果找不到用户名或密码不正确，AbstractUserDetailsAuthenticationProvider会引发BadCredentialsException。
            将此属性设置为false将导致为前者引发UsernameNotFoundExceptions。注意，对于这两个异常，这被认为比抛出BadCredentialsException更不安全。
         */
        provider.setHideUserNotFoundExceptions(false);
        provider.setUserDetailsService(userDetailsService);
        /*
            设置用于编码和验证密码的PasswordEncoder实例。如果未设置，将使用PasswordEncoderFactories.createDelegatingPasswordEncoder();
         */
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    //配置用户信息获取来源、密码校验规则
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        auth.authenticationProvider(authenticationProvider());
    }

    //密码编码器，使用BCrypt加密算法加密
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /*@Bean
    public HttpFirewall allowUrlSemicolonHttpFirewall() {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        //确定URL中是否允许分号（即矩阵变量）。默认设置是禁用此行为，因为这是尝试执行反射文件下载攻击的常见方式。它也是许多绕过基于URL的安全漏洞的来源
        firewall.setAllowSemicolon(true);
        return firewall;
    }*/
}